//
//  DriverViewController.swift
//  Example
//
//  Created by T AO on 2022/2/14.
//

import UIKit
import RxSwift
import RxCocoa

private let screenW = UIScreen.main.bounds.width

class DriverViewController: BaseViewController {
    
    let query: UITextField = UITextField(frame: CGRect(x: 10, y: 100, width: screenW - 20, height: 50))
    let resultCount: UILabel = UILabel(frame: CGRect(x: 10, y: 200, width: screenW - 20, height: 50))
    let resultsTableView: UITableView = UITableView(frame: CGRect(x: 10, y: 300, width: screenW - 20, height: 300))

    override func viewDidLoad() {
        super.viewDidLoad()
        
        makeUI()
        
//        driver1()
//        driver2()
        
        driver()
    }
    
    private func makeUI() {
        query.backgroundColor = .red
        view.addSubview(query)
        
        resultCount.backgroundColor = .orange
        view.addSubview(resultCount)
        
        resultsTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(resultsTableView)
    }
}

/// Driver 可以说是最复杂的 trait，它的目标是提供一种简便的方式在 UI 层编写响应式代码
/// 如果我们的序列满足如下特征，就可以使用它：
/// 不会产生 error 事件
/// 一定在主线程监听（MainScheduler）
/// 共享状态变化（shareReplayLatestWhileConnected）
///
/// 为什么要使用 Driver?
/// （1）Driver 最常使用的场景应该就是需要用序列来驱动应用程序的情况了，比如：
///     - 通过 CoreData 模型驱动 UI
///     - 使用一个 UI 元素值（绑定）来驱动另一个 UI 元素值
/// （2）与普通的操作系统驱动程序一样，如果出现序列错误，应用程序将停止响应用户输入
/// （3）在主线程上观察到这些元素也是极其重要的，因为 UI 元素和应用程序逻辑通常不是线程安全的
/// （4）此外，使用构建 Driver 的可观察的序列，它是共享状态变化
///
extension DriverViewController {
    /// 官方提供的样例，大致意思是：根据一个输入框的关键字来请求数据，然后将获取到的结果绑定到另一个 Label 和 TableView 中
    private func driver1() {
        // 初学者使用 Observable 序列加 bindTo 绑定来实现这个功能的话可能会这么写
        let results = query.rx.text
            // 在主线程中操作，0.3秒内值若多次改变，取最后一次
            .throttle(RxTimeInterval.milliseconds(300), scheduler: MainScheduler.instance)
            .flatMapLatest { query in
                // 筛选出空值，拍平序列
                getPlayList(query ?? "0")
            }
        
        print("results: \(results)")
        
        // 将返回的结果绑定到用于显示结果数量的 label 上
        results
            .map { "\($0.count)" }
            .bind(to: resultCount.rx.text)
            .disposed(by: disposeBag)
        
        // 将返回的结果绑定到 tableView 上
        results
            .bind(to: resultsTableView.rx.items(cellIdentifier: "cell")) { _, result, cell in
                cell.textLabel?.text = "\(result)"
            }
            .disposed(by: disposeBag)
    }
    
    /// 上面 driver1 代码会存在3个问题
    ///
    /// 如果 getPlayList 的序列产生了一个错误（网络请求失败），这个错误将取消所有绑定。此后用户再输入一个新的关键字时，是无法发起新的网络请求
    /// 如果 getPlayList 在后台返回序列，那么刷新页面也会在后台进行，这样就会出现异常崩溃
    /// 返回的结果被绑定到两个 UI 元素上，那就意味着，每次用户输入一个新的关键字时，就会分别为两个 UI 元素发起 HTTP 请求，这并不是我们想要的结果
    ///
    /// 所以对上面 driver1 代码进行修改
    private func driver2() {
        let results = query.rx.text
            .throttle(RxTimeInterval.milliseconds(300), scheduler: MainScheduler.instance)
            .flatMapLatest { query in
                getPlayList(query ?? "0")
                    .observe(on: MainScheduler.instance) // 将返回结果切换到主线程上
                    .catchAndReturn([:]) // 错误被处理了，这样啊至少不会终止整个序列
            }
            .share(replay: 1) // 共享 HTTP 请求
        
        results
            .map { "\($0.count)" }
            .bind(to: resultCount.rx.text)
            .disposed(by: disposeBag)
        
        results
            .bind(to: resultsTableView.rx.items(cellIdentifier: "cell")) { _, result, cell in
                cell.textLabel?.text = "\(result)"
            }
            .disposed(by: disposeBag)
    }
    
    /// 终极解决方案：
    /// 虽然我们通过增加一些额外的处理，让程序可以正确运行。到对于一个大型的项目来说，如果都这么干也太麻烦了，而且容易遗漏出错
    /// 而如果我们使用 Driver 来实现的话就简单了
    private func driver() {
        let results = query.rx.text
            .asDriver()
            .throttle(RxTimeInterval.milliseconds(300))
            .flatMapLatest { query in
                getPlayList(query ?? "0")
                    .asDriver(onErrorJustReturn: [:]) // 仅仅提供发生错误时的备选返回值
            }
        
        results
            .map { "\($0.count)" }
            .drive(resultCount.rx.text) // 这里使用 drive 而不是 bind
            .disposed(by: disposeBag)
        
        results
            .drive(resultsTableView.rx.items(cellIdentifier: "cell")) { _, result, cell in
                cell.textLabel?.text = "\(result)"
            }
            .disposed(by: disposeBag)
        
    }
}

/// 获取豆瓣某频道下的歌曲信息
private func getPlayList(_ channel: String) -> Single<[String: Any]> {
    return Single<[String: Any]>.create { single in
        let url = "https://fm.douban.com/j/mine/playlist?type=n&channel=\(channel)&from=mainsite"
        let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, error in
            if let error = error {
                single(.failure(error))
                return
            }
            
            guard let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves), let result = json as? [String: Any] else {
                single(.failure(DataError.cantParseJSON))
                return
            }
            
            single(.success(result))
        }
        
        task.resume()
        
        return Disposables.create { task.cancel() }
    }
}
